﻿using System;
using System.Collections.Generic;
using System.Text;

namespace Kyoh.Lib.IO.Archive
{
    /// <summary>Zipファイル内のファイル名の <c>({Decode}, ({Encode}, {Decode})*)</c> または <c>({Encode}, ({Decode}, {Encode})*)</c> という繰り返しを実現するクラスです。</summary>
    public class ZipEncodingLinkedSequence : IZipEncodingSequence
    {
        /// <summary>シーケンスの1つめの <see cref="IZipConverter"/> を取得します。</summary>
        public ZipEncoding Header { get; private set; }
        /// <summary>シーケンスの2つめの <see cref="IZipConverter"/> を取得します。</summary>
        public ZipEncoding Center { get; private set; }
        /// <summary>シーケンスの3つめ以降の <see cref="IZipEncodingSequence"/> を取得します。</summary>
        public ZipEncodingLinkedSequence Tailer { get; private set; }

        /// <summary><see cref="IZipEncodingSequence"/> クラスの新しいインスタンスを初期化します。</summary>
        /// <param name="header">シーケンスの1つめに設定する <see cref="ZipEncoding"/>。</param>
        public ZipEncodingLinkedSequence(ZipEncoding header) : this(header, null, (ZipEncodingLinkedSequence)null) { }

        /// <summary><see cref="IZipEncodingSequence"/> クラスの新しいインスタンスを初期化します。</summary>
        /// <param name="header">シーケンスの1つめに設定する <see cref="ZipEncoding"/>。</param>
        /// <param name="center">シーケンスの2つめに設定する <see cref="ZipEncoding"/>。</param>
        /// <param name="tailer">シーケンスの3つめに設定する <see cref="ZipEncoding"/>。</param>
        public ZipEncodingLinkedSequence(ZipEncoding header, ZipEncoding center, ZipEncoding tailer) : this(header, center, new ZipEncodingLinkedSequence(tailer)) { }

        /// <summary><see cref="IZipEncodingSequence"/> クラスの新しいインスタンスを初期化します。</summary>
        /// <param name="header">シーケンスの1つめに設定する <see cref="ZipEncoding"/>。</param>
        /// <param name="center">シーケンスの2つめに設定する <see cref="ZipEncoding"/>。</param>
        /// <param name="tailer">シーケンスの3つめ以降に設定する <see cref="ZipEncodingLinkedSequence"/>。</param>
        public ZipEncodingLinkedSequence(ZipEncoding header, ZipEncoding center, ZipEncodingLinkedSequence tailer)
        {
            if (header == null)
                throw new ArgumentNullException("header");
            if (center == null && tailer != null)
                throw new ArgumentNullException("converter1");
            if (center != null && tailer == null)
                throw new ArgumentNullException("converter2");

            this.Header = header;
            this.Center = center;
            this.Tailer = tailer;

            if (this.Contains(this))
                throw new RecursiveChainException();
        }

        /// <summary>指定された <see cref="IZipEncodingSequence"/> が、内包関係になっていないかをチェックします。</summary>
        /// <param name="encSequence">内容を確認する  <see cref="IZipEncodingSequence"/>。</param>
        /// <returns>このオブジェクトに、 <paramref name="encSequence"/> が含まれていた場合、<c>true</c>。それ以外の場合は <c>false</c>。</returns>
        private bool Contains(ZipEncodingLinkedSequence encSequence)
        {
            if (encSequence == null)
                return false;
            if (encSequence == this.Tailer)
                return true;

            return this.Tailer != null
                && this.Tailer.Contains(encSequence);
        }

        /// <summary>シーケンスにIZipConverterを追加します。</summary>
        /// <param name="converter1">シーケンスの1つ後に追加する <see cref="IZipConverter"/>。</param>
        /// <param name="converter2">シーケンスの2つ後に追加する <see cref="IZipConverter"/>。</param>
        public void AddEncoding(IZipConverter converter1, IZipConverter converter2)
        {
            if (converter1 == null)
                throw new ArgumentNullException("converter1");
            if (converter2 == null)
                throw new ArgumentNullException("converter2");

            if (this.Center == null)
            {
                if (!(converter1 is ZipEncoding))
                    throw new ArgumentException("converter1");

                if (converter2 is ZipEncoding)
                    this.Tailer = new ZipEncodingLinkedSequence((ZipEncoding)converter2);
                else if (converter2 is ZipEncodingLinkedSequence)
                {
                    this.Tailer = (ZipEncodingLinkedSequence)converter2;
                    if (this.Contains(this))
                    {
                        this.Tailer = null;
                        throw new RecursiveChainException();
                    }
                }
                else
                    throw new ArgumentException("converter2");
                this.Center = (ZipEncoding)converter1;
            }
            else
                this.Tailer.AddEncoding(converter1, converter2);
        }

        #region IEnumerable<IZipConverter> メンバー

        /// <summary>シーケンスに含まれる <see cref="IZipConverter"/> を列挙します。</summary>
        /// <returns>シーケンスに含まれる <see cref="IZipConverter"/> を列挙する <see cref="IEnumerator{T}"/>。</returns>
        public IEnumerator<IZipConverter> GetEnumerator()
        {
            return this.getEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.getEnumerator();
        }

        private IEnumerator<IZipConverter> getEnumerator()
        {
            yield return this.Header;
            if (this.Center != null)
            {
                yield return this.Center;
                foreach (var child in this.Tailer)
                    yield return child;
            }
        }

        #endregion

        #region IZipConverter メンバー

        // byte配列を、デコード→エンコードしてバイト数を調べる
        private int GetByteCount(bool utf8Entry, byte[] buf)
        {
            return this.Tailer.GetByteCount(utf8Entry,
                   this.Center.GetChars(utf8Entry, buf));
        }

        /// <summary>指定した<see cref="String"/> に含まれる文字をエンコードすることによって生成されるバイト数を計算します。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="s">エンコード対象の文字のセットを格納している<see cref="String"/>。</param>
        /// <returns>指定した文字をエンコードすることによって生成されるバイト数。</returns>
        /// <exception cref="ArgumentNullException">s が <c>null</c> です。</exception>
        /// <exception cref="EncoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="EncoderFallback"/> が <see cref="EncoderExceptionFallback"/> に設定されています。 </exception>
        public int GetByteCount(bool utf8Entry, string s)
        {
            return (this.Center == null)
                ? this.Header.GetByteCount(utf8Entry, s)
                : this.GetByteCount(utf8Entry, this.Header.GetBytes(utf8Entry, s));
        }

        /// <summary>指定した文字配列に格納されているすべての文字をエンコードすることによって生成されるバイト数を計算します。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="chars">エンコード対象の文字を格納している文字配列。</param>
        /// <returns>指定した文字配列に格納されているすべての文字をエンコードすることによって生成されるバイト数。</returns>
        /// <exception cref="ArgumentNullException"><paramref name="chars"/> が <c>null</c> です。</exception>
        /// <exception cref="EncoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="EncoderFallback"/> が <see cref="EncoderExceptionFallback"/> に設定されています。 </exception>
        public int GetByteCount(bool utf8Entry, char[] chars)
        {
            return (this.Center == null)
                ? this.Header.GetByteCount(utf8Entry, chars)
                : this.GetByteCount(utf8Entry, this.Header.GetBytes(utf8Entry, chars));
        }

        /// <summary>指定した文字配列に格納されている文字のセットをエンコードすることによって生成されるバイト数を計算します。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="chars">エンコード対象の文字のセットを格納している文字配列。</param>
        /// <param name="index">エンコードする最初の文字のインデックス。</param>
        /// <param name="count">エンコードする文字数。</param>
        /// <returns>指定した文字をエンコードすることによって生成されるバイト数。</returns>
        /// <exception cref="ArgumentNullException"><paramref name="chars"/> が <c>null</c> です。</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> または <paremref name="count"/> が <c>0</c> 未満です。 または <paramref name="index"/> および <paramref name="count"/> が <paramref name="chars"/> 内の有効な範囲を示していません。</exception>
        /// <exception cref="EncoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="EncoderFallback"/> が <see cref="EncoderExceptionFallback"/> に設定されています。 </exception>
        public int GetByteCount(bool utf8Entry, char[] chars, int index, int count)
        {
            return (this.Center == null)
                ? this.Header.GetByteCount(utf8Entry, chars, index, count)
                : this.GetByteCount(utf8Entry, this.Header.GetBytes(utf8Entry, chars, index, count));
        }

        // char配列を、エンコード→デコードして文字数を調べる
        private int GetCharCount(bool utf8Entry, char[] arr)
        {
            return this.Tailer.GetCharCount(utf8Entry,
                    this.Center.GetBytes(utf8Entry, arr));
        }

        /// <summary>指定したバイト配列に格納されているすべてのバイトをデコードすることによって生成される文字数を計算します。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="bytes">デコード対象のバイト シーケンスが格納されたバイト配列。</param>
        /// <returns>指定したバイト シーケンスをデコードすることによって生成される文字数。</returns>
        /// <exception cref="ArgumentNullException"><paremref name="bytes"/> が <c>null</c> です。</exception>
        /// <exception cref="DecoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="DecoderFallback"/> が <see cref="DecoderExceptionFallback"/> に設定れています。 </exception>
        public int GetCharCount(bool utf8Entry, byte[] bytes)
        {
            return (this.Center == null)
                ? this.Header.GetCharCount(utf8Entry, bytes)
                : this.GetCharCount(utf8Entry, this.Header.GetChars(utf8Entry, bytes));
        }

        /// <summary>指定したバイト配列に格納されているバイト シーケンスをデコードすることによって生成される文字数を計算します。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="bytes">デコード対象のバイト シーケンスが格納されたバイト配列。</param>
        /// <param name="index">デコードする最初のバイトのインデックス。</param>
        /// <param name="count">デコードするバイト数。</param>
        /// <returns>指定したバイト シーケンスをデコードすることによって生成される文字数。</returns>
        /// <exception cref="ArgumentNullException"><paremref name="bytes"/> が <c>null</c> です。</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> または <paremref name="count"/> が <c>0</c> 未満です。 または <paramref name="index"/> および <paramref name="count"/> が <paramref name="bytes"/> 内の有効な範囲を示していません。</exception>
        /// <exception cref="DecoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="DecoderFallback"/> が <see cref="DecoderExceptionFallback"/> に設定れています。 </exception>
        public int GetCharCount(bool utf8Entry, byte[] bytes, int index, int count)
        {
            return (this.Center == null)
                ? this.Header.GetCharCount(utf8Entry, bytes, index, count)
                : this.GetCharCount(utf8Entry, this.Header.GetChars(utf8Entry, bytes, index, count));
        }

        // byte配列を、デコード→エンコードしてbyte配列を取得する
        private byte[] GetBytes(bool utf8Entry, byte[] buf)
        {
            return (this.Center == null)
                ? buf
                : this.Tailer.GetBytes(utf8Entry,
                    this.Center.GetChars(utf8Entry, buf));
        }

        /// <summary>指定した文字配列に格納されているすべての文字をバイト シーケンスにエンコードします。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="chars">エンコード対象の文字を格納している文字配列。</param>
        /// <returns>指定した文字のセットをエンコードした結果を格納しているバイト配列。</returns>
        /// <exception cref="ArgumentNullException"><paramref name="chars"/> が <c>null</c> です。</exception>
        /// <exception cref="EncoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="EncoderFallback"/> が <see cref="EncoderExceptionFallback"/> に設定されています。 </exception>
        public byte[] GetBytes(bool utf8Entry, char[] chars)
        {
            return this.GetBytes(utf8Entry, this.Header.GetBytes(utf8Entry, chars));
        }

        /// <summary>指定した文字配列に格納されている文字のセットをバイト シーケンスにエンコードします。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="chars">エンコード対象の文字のセットを格納している文字配列。</param>
        /// <param name="index">エンコードする最初の文字のインデックス。</param>
        /// <param name="count">エンコードする文字数。</param>
        /// <returns>指定した文字のセットをエンコードした結果を格納しているバイト配列。</returns>
        /// <exception cref="ArgumentNullException"><paramref name="chars"/> が <c>null</c> です。</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> または <paremref name="count"/> が <c>0</c> 未満です。 または <paramref name="index"/> および <paramref name="count"/> が <paramref name="chars"/> 内の有効な範囲を示していません。</exception>
        /// <exception cref="EncoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="EncoderFallback"/> が <see cref="EncoderExceptionFallback"/> に設定されています。 </exception>
        public byte[] GetBytes(bool utf8Entry, char[] chars, int index, int count)
        {
            return this.GetBytes(utf8Entry, this.Header.GetBytes(utf8Entry, chars, index, count));
        }

        /// <summary>指定した<see cref="String"/> に含まれるすべての文字をバイト シーケンスにエンコードします。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="s">エンコード対象の文字のセットを格納している<see cref="String"/>。</param>
        /// <returns>指定した文字のセットをエンコードした結果を格納しているバイト配列。</returns>
        /// <exception cref="ArgumentNullException"><paramref name="s"/> が <c>null</c> です。</exception>
        /// <exception cref="EncoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="EncoderFallback"/> が <see cref="EncoderExceptionFallback"/> に設定されています。 </exception>
        public byte[] GetBytes(bool utf8Entry, string s)
        {
            return this.GetBytes(utf8Entry, this.Header.GetBytes(utf8Entry, s));
        }

        // char配列を、エンコード→デコードしてchar配列を取得する
        private char[] GetChars(bool utf8Entry, char[] buf)
        {
            return (this.Center == null)
                ? buf
                : this.Tailer.GetChars(utf8Entry,
                    this.Center.GetBytes(utf8Entry, buf));
        }

        /// <summary>指定したバイト配列に格納されているすべてのバイトを文字のセットにデコードします。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="bytes">デコード対象のバイト シーケンスが格納されたバイト配列。</param>
        /// <returns>指定したバイト シーケンスのデコード結果が格納された文字配列。</returns>
        /// <exception cref="ArgumentNullException"><paremref name="bytes"/> が <c>null</c> です。</exception>
        /// <exception cref="DecoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="DecoderFallback"/> が <see cref="DecoderExceptionFallback"/> に設定れています。 </exception>
        public char[] GetChars(bool utf8Entry, byte[] bytes)
        {
            return this.GetChars(utf8Entry, this.Header.GetChars(utf8Entry, bytes));
        }

        /// <summary>指定したバイト配列に格納されているバイト シーケンスを文字のセットにデコードします。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="bytes">デコード対象のバイト シーケンスが格納されたバイト配列。</param>
        /// <param name="index">デコードする最初のバイトのインデックス。</param>
        /// <param name="count">デコードするバイト数。</param>
        /// <returns>指定したバイト シーケンスのデコード結果が格納された文字配列。</returns>
        /// <exception cref="ArgumentNullException"><paremref name="bytes"/> が <c>null</c> です。</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> または <paremref name="count"/> が <c>0</c> 未満です。 または <paramref name="index"/> および <paramref name="count"/> が <paramref name="bytes"/> 内の有効な範囲を示していません。</exception>
        /// <exception cref="DecoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="DecoderFallback"/> が <see cref="DecoderExceptionFallback"/> に設定れています。 </exception>
        public char[] GetChars(bool utf8Entry, byte[] bytes, int index, int count)
        {
            return this.GetChars(utf8Entry, this.Header.GetChars(utf8Entry, bytes, index, count));
        }

        // string を、エンコード→デコードして string を取得する
        private string GetString(bool utf8Entry, string str)
        {
            return (this.Center == null)
                ? str
                : this.Tailer.GetString(utf8Entry,
                    this.Center.GetBytes(utf8Entry, str));
        }

        /// <summary>指定したバイト配列に格納されているすべてのバイトを文字列にデコードします。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="bytes">デコード対象のバイト シーケンスが格納されたバイト配列。</param>
        /// <returns>指定したバイト シーケンスのデコード結果が格納されている<see cref="String"/>。</returns>
        /// <exception cref="ArgumentNullException"><paremref name="bytes"/> が <c>null</c> です。</exception>
        /// <exception cref="DecoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="DecoderFallback"/> が <see cref="DecoderExceptionFallback"/> に設定れています。 </exception>
        public string GetString(bool utf8Entry, byte[] bytes)
        {
            return this.GetString(utf8Entry, this.Header.GetString(utf8Entry, bytes));
        }

        /// <summary>指定したバイト配列に格納されているバイト シーケンスを文字列にデコードします。</summary>
        /// <param name="utf8Entry">エントリのUTF-8フラグがオンであるかどうかを示す <see cref="Boolean"/> 値。</param>
        /// <param name="bytes">デコード対象のバイト シーケンスが格納されたバイト配列。</param>
        /// <param name="index">デコードする最初のバイトのインデックス。</param>
        /// <param name="count">デコードするバイト数。</param>
        /// <returns>指定したバイト シーケンスのデコード結果が格納される<see cref="String"/>。</returns>
        /// <exception cref="ArgumentNullException"><paremref name="bytes"/> が <c>null</c> です。</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> または <paremref name="count"/> が <c>0</c> 未満です。 または <paramref name="index"/> および <paramref name="count"/> が <paramref name="bytes"/> 内の有効な範囲を示していません。</exception>
        /// <exception cref="DecoderFallbackException">フォールバックが発生しました (詳細については、「エンコーディングの理解」を参照してください)。 および <see cref="DecoderFallback"/> が <see cref="DecoderExceptionFallback"/> に設定れています。 </exception>
        public string GetString(bool utf8Entry, byte[] bytes, int index, int count)
        {
            return this.GetString(utf8Entry, this.Header.GetString(utf8Entry, bytes, index, count));
        }

        #endregion

        /// <summary><see cref="ZipEncodingLinkedSequence"/> の文字列表現を取得します。</summary>
        /// <returns><see cref="ZipEncodingLinkedSequence"/> の文字列表現。</returns>
        public override string ToString()
        {
            return Header.ToString() + (this.Center == null ? string.Empty : " -> " + this.Center.ToString() + " -> " + this.Tailer.ToString());
        }
    }
}
